<?php
declare(strict_types=1);

namespace LowCode;

use BadMethodCallException;
use Illuminate\Support\Collection;
use LowCode\Support\Fluent;
use LowCode\Support\JsApi;
use LowCode\Support\JsDs;
use LowCode\Support\JsExp;
use LowCode\Support\JsFn;
use LowCode\Support\JsWatch;
use LowCode\UI\JsonNode;

/**
 * @property-read Collection $state
 * @property-read Collection $methods
 * @property-read Collection $watch
 * @property-read Collection $computed
 * @property-read Collection $dataSources
 * @property-read Collection $jsonNode
 * @method static Store setId(string $id = null)
 * @method static Store setState(string $key, mixed $value = null)
 * @method static Store setMethod(string $name, string $value, string $id = null)
 * @method static Store setWatch(JsFn|JsExp $source, string|JsFn $handler = "(newVal, oldVal)=>{}", array $options = [])
 * @method static Store setComputed(string $name, JsFn $value)
 * @method static Store setDataSource(string $name, JsDs $dataSource)
 * @method static Store setApi(JsApi $jsApi)
 * @method static Store setMeta(array $meta)
 * @method static Store setBlock(Block $block)
 * @method static Store setDependencies(Dependencies $dependencies)
 * @method static Store setCss(string $link)
 * @method static Store setJs(string $link)
 * @method static Store setScript(string $script)
 */
class Store extends Fluent
{
    private static Store $instance;

    protected string $id = "global";

    public static array $staticMaps = [
        "setId"=>"id",
        "setState"=>"state",
        "setMethod"=>"method",
        "setWatch"=>"watch",
        "setComputed"=>"computed",
        "setDataSource"=>"dataSource",
        "setApi"=>"addApi",
        "setMeta"=>"addMeta",
        "setBlock"=>"addBlock",
        "setDependencies"=>"addDependencies",
        "setCss"=>"addCss",
        "setJs"=>"addJs",
        "setScript"=>"addScript",
    ];

    public function __construct(string $id = null){
        parent::__construct([
            "state"=>Collection::make(),
            "methods"=>Collection::make(),
            "watch"=>Collection::make(),
            "computed"=>Collection::make(),
            "dataSources"=>Collection::make(),
            "jsonNode"=>Collection::make(),
        ]);
        $this->id($id);
    }

    /**
     * @param string|null $id
     * @return static
     */
    public static function make(string $id = null): static
    {
        if (!isset(self::$instance) || !(self::$instance instanceof self)) {
            self::$instance = new self($id);
        }
        return self::$instance->id($id);
    }

    /**
     * @return string
     */
    public function getId(): string
    {
        return $this->id;
    }

    /**
     * @param string|null $id
     * @return $this
     */
    public function id(string $id = null): static
    {
        if ($id) $this->id = $id;
        return $this;
    }

    /**
     * @return array
     */
    public function getState(): array
    {
        return $this->state->get($this->id, []);
    }

    /**
     * @return array
     */
    private function getMethods(): array
    {
        return $this->methods->get($this->id, []);
    }

    /**
     * @return array
     */
    private function getWatch(): array
    {
        return $this->watch->get($this->id, []);
    }

    /**
     * @return array
     */
    private function getComputed(): array
    {
        return $this->computed->get($this->id, []);
    }

    /**
     * @return array
     */
    private function getDataSources(): array
    {
        return $this->dataSources->get($this->id, []);
    }

    /**
     * @return JsonNode
     */
    private function getJsonNode(): JsonNode
    {
        return $this->jsonNode->get($this->id, JsonNode::make());
    }

    /**
     * 设置state状态数据
     * @param string $key
     * @param mixed|JsExp|JsFn|null $value
     * @return $this
     */
    public function state(string $key, mixed $value = null): static
    {
        $state = $this->getState();
        $state[$key] = $value;
        $this->state->put($this->id, $state);
        return $this;
    }

    /**
     * 设置state状态数据
     * @param array $states
     * @return $this
     */
    public function states(array $states): static
    {
        $state = $this->getState();
        $state = array_merge($state, $states);
        $this->state->put($this->id, $state);
        return $this;
    }

    /**
     * 删除state其中某个属性状态数据
     * @param string $key
     * @return $this
     */
    public function removeState(string $key): static
    {
        $state = $this->getState();
        if(isset($state[$key])){
            unset($state[$key]);
        }
        $this->state->put($this->id, $state);
        return $this;
    }

    /**
     * 添加组件方法method
     * @param string $name 方法名称
     * @param string $value js函数脚本
     * @param string|null $id
     * @return $this
     */
    public function method(string $name, string $value, string $id = null): static
    {
        $methods = $this->getMethods();
        $methods[$name] = JsFn::make($value, $id);
        $this->methods->put($this->id, $methods);
        return $this;
    }

    /**
     * 添加组件watch
     * @param JsFn|JsExp $source
     * @param string|JsFn $handler (newVal, oldVal)=>{}
     * @param array $options
     * @return $this
     */
    public function watch(JsFn|JsExp $source, string|JsFn $handler = "(newVal, oldVal)=>{}", array $options = []): static
    {
        $watch = $this->getWatch();
        if (!$handler instanceof JsFn){
            $handler = JsFn::make($handler);
        }
        $watch[] = JsWatch::make($source, $handler, $options);
        $this->watch->put($this->id, $watch);
        return $this;
    }

    /**
     * 添加组件计算属性computed
     * @param string $name
     * @param JsFn $value
     * @return $this
     */
    public function computed(string $name, JsFn $value): static
    {
        $computed = $this->getComputed();
        $computed[$name] = $value;
        $this->computed->put($this->id, $computed);
        return $this;
    }

    /**
     * 添加组件源
     * @param string $name
     * @param JsDs $dataSource
     * @return $this
     */
    public function dataSource(string $name, JsDs $dataSource): static
    {
        $dataSources = $this->getDataSources();
        $dataSources[$name] = $dataSource;
        $this->dataSources->put($this->id, $dataSources);
        return $this;
    }

    private function addToNode(string $key, mixed $value): static
    {
        $node = $this->getJsonNode();
        $options = $node->get($key,[]);
        $options[] = $value;
        $node->setAttribute($key, $options);
        $this->jsonNode->put($this->id, $node);
        return $this;
    }

    public function addApi(JsApi $jsApi): static
    {
        return $this->addToNode("apis", $jsApi);
    }

    public function addMeta(array $meta): static
    {
        return $this->addToNode("meta", $meta);
    }

    public function addBlock(Block $block): static
    {
        return $this->addToNode("blocks", $block);
    }

    public function addDependencies(Dependencies $dependencies): static
    {
        return $this->addToNode("dependencies", $dependencies);
    }

    private function addAsset(string $name, string $value): static
    {
        $node = $this->getJsonNode();
        $assets = $node->get("assets",[]);
        $options = $assets[$name]??[];
        $options[] = $value;
        $assets[$name] = $options;
        $node->setAttribute("assets", $assets);
        $this->jsonNode->put($this->id, $node);
        return $this;
    }

    public function addCss(string $link): static
    {
        return $this->addAsset("css",$link);
    }

    public function addJs(string $link): static
    {
        return $this->addAsset("js",$link);
    }

    public function addScript(string $script): static
    {
        return $this->addAsset("script",$script);
    }


    public function toArray(): array
    {
        return [
            "state"=>$this->getState(),
            "methods"=>$this->getMethods(),
            "watch"=>$this->getWatch(),
            "computed"=>$this->getComputed(),
            "dataSources"=>$this->getMethods(),
        ];
    }

    public function buildBlock(Block $block): void
    {
        $store = $this->toArray();
        if ($store['state']){
            $block->states($store['state']);
        }
        if ($store['methods']){
            $block->methods($store['methods']);
        }
        if ($store['watch']){
            $block->watchList($store['watch']);
        }
        if ($store['computed']){
            $block->computedList($store['computed']);
        }
        if ($store['dataSources']){
            $block->dataSources($store['dataSources']);
        }
    }

    public function buildPage(Page $page, JsonNode $jsonNode): void
    {
        $page->apis($this->mergeJsonNode($jsonNode, "apis"));
        $page->meta($this->mergeJsonNode($jsonNode, "meta"));
        $page->blocks($this->mergeJsonNode($jsonNode, "blocks"));
        $page->dependencies($this->mergeJsonNode($jsonNode, "dependencies"));
        $page->assets($this->mergeJsonNode($jsonNode, "assets"));
    }

    /**
     * @param JsonNode $jsonNode
     * @param string $field
     * @return array
     */
    private function mergeJsonNode(JsonNode $jsonNode, string $field): array
    {
        $props = $jsonNode->get("props",[]);
        $options1 = $props[$field]??[];
        $options2 = $this->getJsonNode()->get($field, []);
        if ($options2){
            $props[$field] = array_merge($options1,$options2);
            $jsonNode->setAttribute("props", $props);
        }
        return $props[$field]??[];
    }

    public static function __callStatic(string $method, array $parameters)
    {
        $store = self::make();
        if (isset(self::$staticMaps[$method])){
            $method  = self::$staticMaps[$method];
        }
        if (method_exists($store,$method)){
            $store->$method(...$parameters);
            return $store;
        }
        throw new BadMethodCallException(sprintf(
            'Method %s::%s does not exist.', static::class, $method
        ));
    }


    private function __clone(){

    }
}
